xquery version "1.0";

module namespace oa = "http://fgeorges.org/ns/oauth";

import module namespace crypto = "http://expath.org/ns/crypto";
import module namespace http   = "http://expath.org/ns/http-client";
import module namespace t      = "http://fgeorges.org/ns/oauth/tools" at "tools.xql";

declare variable $oa:linkedin-api :=
   <oa:api oauth-version="1.0">
      <oa:initiate-url>https://api.linkedin.com/uas/oauth/requestToken</oa:initiate-url>
      <oa:authorize-url>https://api.linkedin.com/uas/oauth/authenticate?oauth_token=$1</oa:authorize-url>
      <oa:access-url>https://api.linkedin.com/uas/oauth/accessToken</oa:access-url>
   </oa:api>;

(: return service with oa:client :)
declare function oa:make-service(
   $api           as element(oa:api),
   $client-key    as xs:string,
   $client-secret as xs:string,
   $callback      as xs:string?,
   $scope         as xs:string?
) as element(oa:service)
{
   <oa:service>
      { $api }
      <oa:callback>{ ( $callback, 'oob' )[1] }</oa:callback>
      { <oa:scope>{ $scope }</oa:scope>[$scope] }
      <oa:client>
         <oa:key>{ $client-key }</oa:key>
         <oa:secret>{ $client-secret }</oa:secret>
      </oa:client>
   </oa:service>
};

declare function oa:base-string($request as element(http:request), $oauth-params as element(params))
   as xs:string
{
   let $query    := t:uri-query-string($request/@href)
   let $from-url := if ( exists($query[.]) ) then t:parse-form-urlencoded($query) else ()
   let $all      := ( $from-url, $oauth-params )
   let $ordered  := for $p in $all/param order by $p/@name, $p return $p
   return
      string-join((
         encode-for-uri(upper-case($request/@method)),
         (: TODO: Check (and remove) the port if 80 for HTTP or 443 for HTTPS. :)
         (: TODO: Must remove query string if any. :)
         (: TODO: Domain name must be in lower case. :)
         encode-for-uri($request/@href),
         encode-for-uri(
            string-join(
               $ordered/concat(encode-for-uri(@name), '=', encode-for-uri(.)),
               '&amp;'))),
         '&amp;')
};

declare function oa:signature($base-string as xs:string, $service as element(oa:service), $token as element()?)
   as xs:string
{
   xs:string(
      crypto:hmac-sha1(
         $base-string,
         concat(
            encode-for-uri($service/oa:client/oa:secret),
            '&amp;',
            encode-for-uri($token/oa:secret))))
};

declare function oa:parse-response($response as xs:string)
   as element()+ (: oa:key, oa:secret :)
{
   let $parsed := t:parse-form-urlencoded($response)
   let $key    := $parsed/param[@name eq 'oauth_token']
   let $secret := $parsed/param[@name eq 'oauth_token_secret']
   return
      if ( empty($key) ) then
         error((), 'Key not in the response...')
      else if ( empty($secret) ) then
         error((), 'Secret not in the response...')
      else (
         <oa:key>{ $key/string(.) }</oa:key>,
         <oa:secret>{ $secret/string(.) }</oa:secret>
      )
};

(:~
 : TODO: How to know whether to put the authorization as a header or in the query string?
 :)
declare function oa:send-http-impl(
   $request as element(http:request),
   $service as element(oa:service),
   $params  as element(params),
   $token   as element()?
) as item()+
{
   let $base-string := oa:base-string($request, $params)
   let $signature   := oa:signature($base-string, $service, $token)
   let $header      :=
         concat(
            string-join($params/param/concat(@name, '="', encode-for-uri(.), '"'), ', '),
            ', oauth_signature="',
            encode-for-uri($signature),
            '"')
   return
      http:send-request(
         element { node-name($request) } {
            $request/@*,
            <http:header name="Authorization" value='OAuth { $header }'/>,
            $request/(node() except http:header[lower-case(@name) eq 'authorization'])
         })
};

(: return service with oa:temporary :)
declare function oa:initiate($service as element(oa:service))
   as element(oa:service)
{
   let $params   :=
         <params>
            <param name="oauth_version">{ $service/oa:api/string(@oauth-version) }</param>
            <param name="oauth_callback">{ $service/string(oa:callback) }</param>
            <param name="oauth_consumer_key">{ $service/oa:client/string(oa:key) }</param>
            <param name="oauth_timestamp">{ t:current-timestamp() }</param>
            <param name="oauth_nonce">xxxTODOxxxFINDANONCExxxBASEDonSOMErandomNUMBERSxxx</param>
            <param name="oauth_signature_method">HMAC-SHA1</param>
            { <param name="scope">{ $service/string(oa:scope) }</param>[$service/oa:scope] }
         </params>
   let $response :=
         oa:send-http-impl(
            <http:request href="{ $service/oa:api/string(oa:initiate-url) }" method="post">
               <http:body media-type="application/x-www-form-urlencoded">{ ' ' }</http:body>
            </http:request>,
            $service,
            $params,
            ())
   return
      (: TODO: Check $response[1] value (200?, text?, etc.) :)
      if ( not($response[1]/xs:integer(@status) eq 200) ) then
         (:error((), 'HTTP return status not 200...', $response[1]):)
         error((), concat('HTTP return status not 200... - ', $response[2]))
      else
         <oa:service> {
            $service/*,
            <oa:temporary> {
               oa:parse-response($response[2])
            }
            </oa:temporary>
         }
         </oa:service>
};

(: service must have oa:temporary :)
declare function oa:authorize-url($service as element(oa:service))
   as xs:string
{
   let $key     := $service/oa:temporary/oa:key
   let $pattern := $service/oa:api/oa:authorize-url
   return
      if ( not($key[.]) ) then
         error((), 'Empty temporary key not allowed')
      else if ( not($pattern[.]) ) then
         error((), 'Empty authorize url pattern not allowed')
      else
         replace($pattern, '\$1', $key)
};

(: return service with oa:token, must have oa:temporary :)
declare function oa:authorize($service as element(oa:service), $verifier as xs:string)
   as element(oa:service)
{
   let $params :=
         <params>
            <param name="oauth_version">{ $service/oa:api/string(@oauth-version) }</param>
            <param name="oauth_consumer_key">{ $service/oa:client/string(oa:key) }</param>
            <param name="oauth_timestamp">{ t:current-timestamp() }</param>
            <param name="oauth_nonce">xxxTODOxxxFINDANONCExxxBASEDonSOMErandomNUMBERSxxx</param>
            <param name="oauth_signature_method">HMAC-SHA1</param>
            <param name="oauth_token">{ $service/oa:temporary/string(oa:key) }</param>
            <param name="oauth_verifier">{ $verifier }</param>
         </params>
   let $response := 
         oa:send-http-impl(
            <http:request href="{ $service/oa:api/oa:access-url }" method="post">
               <http:body media-type="application/x-www-form-urlencoded">{ ' ' }</http:body>
            </http:request>,
            $service,
            $params,
            $service/oa:temporary)
   return
      (: TODO: Check $response[1] value (200?, text?, etc.) :)
      if ( not($response[1]/xs:integer(@status) eq 200) ) then
         error((), 'HTTP return status not 200... ', $response[1])
      else
         <oa:service> {
            $service/*,
            <oa:token> {
               oa:parse-response($response[2])
            }
            </oa:token>
         }
         </oa:service>
};

declare function oa:send-http($request as element(http:request), $service as element(oa:service))
   as item()+
{
   let $params :=
         <params>
            <param name="oauth_version">{ $service/oa:api/string(@oauth-version) }</param>
            <param name="oauth_consumer_key">{ $service/oa:client/string(oa:key) }</param>
            <param name="oauth_timestamp">{ t:current-timestamp() }</param>
            <param name="oauth_nonce">xxxTODOxxxFINDANONCExxxBASEDonSOMErandomNUMBERSxxx</param>
            <param name="oauth_signature_method">HMAC-SHA1</param>
            <param name="oauth_token">{ $service/oa:token/string(oa:key) }</param>
         </params>
   return
      oa:send-http-impl($request, $service, $params, $service/oa:token)
};
